﻿// Copyright (c) 2010 Dan Popick

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;

using Open.Web.Stijl.Css;
using Open.Web.Stijl.Partitioning;

namespace Open.Web.Stijl
{
    public abstract class TileEngine
    {
		static Configuration.StijlConfiguration _config = Configuration.StijlConfiguration.Default;
		static Utility.Tracer<TileEngine> _log = new Utility.Tracer<TileEngine>();

		protected string InputCssPath { get; private set; }

        public TileEngine()
        {
        }

        /// <summary>
        /// Places all the sprites into an array
        /// </summary>
        /// <param name="container">A container with a set of sprites to place.</param>
        List<Tile> TileContainer(SpriteContainer container)
        {
            // sort the list of sprites by diagonal length -- largest first
            List<Sprite> spriteList = new List<Sprite>(container.Sprites);
            spriteList.Sort((x, y) => (int)(y.Size.DiagonalLength() - x.Size.DiagonalLength()));

            JaggedBoundary boundary = new JaggedBoundary();
            List<Tile> placements = new List<Tile>();

            // place them all
            spriteList.ForEach(x => Place(x, boundary, placements)); // O(n^3)

            return placements;
        }

		/// <summary>
		/// Tiles all sprite containers in preparation for generating the final
		/// images.
		/// </summary>
		/// <param name="containers">The containers to tile</param>
		/// <returns>A list of TiledImage objects which are ready convert into images.</returns>
		List<TiledImage> Tile(IEnumerable<SpriteContainer> containers)
		{
			List<TiledImage> result = new List<TiledImage>();

			foreach (SpriteContainer sc in containers)
				result.Add(new TiledImage(TileContainer(sc), sc.Colors));

			return result;
		}

		class PSprite : PartitionedItem
		{
			public Sprite Sprite { get; set; }

			private PSprite(int[] factors, int size, int secondarySize): base(factors, size, secondarySize) { }

			static public PSprite FromSprite(Sprite s)
			{
				int primary, secondary;

				if (s.Repeat == RepeatType.RepeatX)
				{
					primary = s.Size.Width;
					secondary = s.Size.Height;
				} else {
					secondary = s.Size.Width;
					primary = s.Size.Height;
				}

				PSprite ps = new PSprite(null, 0, secondary);
				ps.InitializeFromNumber(primary);
				ps.Sprite = s;

				return ps;
			}
		}

		List<TiledImage> TileRepeating(IEnumerable<SpriteContainer> containers)
		{
			// TODO: These are already color aggregated, but this process is premature
			// since it's still possible for these containers to be split based on 
			// their partitioning
			List<TiledImage> result = new List<TiledImage>();

			foreach (SpriteContainer sc in containers)
			{
				// generate psprite working objects for partitioning
				var partitions = from sprite in sc.Sprites
								 select PSprite.FromSprite(sprite);

				// do the partition
				RepeatingPartitioner<PSprite> partitioner = new RepeatingPartitioner<PSprite>();
				PartialSolution<PSprite> solution = partitioner.FindSolution(partitions.ToList());

				// iterate over the solution
				foreach (Set<PSprite> spriteset in solution.AssignedGroups)
				{
					// tile the images
					List<Tile> tiles = new List<Tile>();
					int imageExtent = 0;
					RepeatType repeatType = RepeatType.NotSpecified;
					foreach (PSprite psprite in spriteset)
					{
						int x, y;

						// here we place the images along the edge of the sprite sheet
						// repeating the images across the extend of the image is the
						// responsibility of the rendering class (RepeatingTiledImage),
						// so we don't address the repeating aspect here.
						if (psprite.Sprite.Repeat == RepeatType.RepeatX)
						{
							x = 0;
							y = imageExtent;	
							imageExtent += psprite.Sprite.Size.Height;
						} else {
							y = 0;
							x = imageExtent;
							imageExtent += psprite.Sprite.Size.Width;
						}

						tiles.Add(new Tile(psprite.Sprite, new Point(x, y)));
						repeatType = psprite.Sprite.Repeat;
					}

					result.Add(new RepeatingTiledImage(tiles, sc.Colors, repeatType, spriteset.Size));
				}
			}

			return result;
		}

		/// <summary>
        /// Place the sprite in the list.
        /// </summary>
        /// <param name="sprite">The sprite to place</param>
        /// <param name="boundary">The running placement boundary</param>
        /// <param name="placements">The list of placed sprites</param>
        void Place(Sprite sprite, JaggedBoundary boundary, List<Tile> placements)
        {
            List<double> costs = boundary.GetCostArray(sprite.Size); // O(n^2)

            // This is slightly awkward; we're really just looking for the point with the lowest cost.
            // The GetCostArray method could be changed to just return the index. But the method is
            // more modular this way, and could be used to evaluate alternate solutions in the future.
            var indexedCosts = costs
                .Select((x, i) => new { Index = i, Cost = x })
                .MinElement(x => x.Cost);

            // place the block into the boundary and record the placement in the list.
            Point placementPoint = boundary.Points.ElementAt(indexedCosts.Index);
            boundary.AddRegion(placementPoint, sprite.Size);
            placements.Add(new Tile(sprite, placementPoint));
        }

		/// <summary>
		/// Generates the URL for the image. Must be overridden based on the context (handler/task)
		/// </summary>
		/// <param name="imageIndex">An index for the image, unique for each CSS file.</param>
		/// <param name="image">The tiled image reference.</param>
		/// <returns>The path to the image to be rendered out to the updated CSS file</returns>
		abstract protected string GenerateUrl(int imageIndex, TiledImage image);

        /// <summary>
        /// Processes a full css file into the minified CSS and a set of images
        /// </summary>
        /// <param name="filePath">The path to the file to process</param>
        /// <param name="minifiedCss">The contents of the CSS file slightly minified.></param>
        /// <param name="images">A list of sprited images generated as part of the tiling</param>
        /// <param name="dependentFiles">A list of dependent physical files (for maintaining cache dependencies)</param>
        /// <remarks>
        /// Updating the rules on all the members of the returned image list is the resposibility of the caller
        /// </remarks>
        public virtual void ProcessFile(string filePath, out string minifiedCss, out List<TiledImage> images, out List<string> dependentFiles)
        {
			InputCssPath = filePath;
            CssParser parser = new CssParser();
            dependentFiles = new List<string>();
			List<object> components = parser.Parse(InputCssPath, dependentFiles);

			dependentFiles.Add(InputCssPath);
            DateTime maxDate = dependentFiles.Max(x => (new FileInfo(x)).LastWriteTimeUtc);

            var spriteable = components.OfType<SpriteableRuleSet>().ToList();

			string dir = Path.GetDirectoryName(InputCssPath);
			string fileName = Path.GetFileName(InputCssPath);

			Dictionary<string, Sprite> activeSprites = new Dictionary<string, Sprite>(
				_config.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
			List<Sprite> sprites = new List<Sprite>();

            foreach (SpriteableRuleSet rs in spriteable)
            {
                string url = rs.Url.Unquote();
				string key = String.Format("{0}-{1}", rs.Repeat, url);
                Sprite existingSprite;

                if (activeSprites.TryGetValue(key, out existingSprite))
                {
                    existingSprite.RuleSets.Add(rs);
                }
                else
                {
                    Sprite s = new Sprite(rs);
					sprites.Add(s);
                    string imPath = Path.Combine(dir, url);

                    try
                    {
                        s.LoadImage(imPath);
                        activeSprites[key] = s;
                    }
                    catch
                    {
                        _log.Warn("Unable to load the image '{0}'", imPath);
                    }
                }
            }

            // categorize the sprites to prepare for tiling
            SpriteCategorizer cat = new SpriteCategorizer();
            List<SpriteContainer> containers = cat.Categorize(activeSprites.Values);

            // tile the sprites
			images = new List<TiledImage>(
				Tile(containers.Where(x => x.Repeat == RepeatType.NoRepeat)).Union(
				TileRepeating(containers.Where(x => x.Repeat == RepeatType.RepeatX || x.Repeat == RepeatType.RepeatY))));
			

			GenerateImages(images);

            // render the minified CSS out
            StringWriter sw = new StringWriter();
            components.ForEach(x =>
            {
                CssElement ce = x as CssElement;
                if (ce != null) ce.Write(sw); else sw.Write(x.ToString());
                sw.WriteLine();
            });

			sprites.ForEach(x => x.Dispose());

            minifiedCss = sw.ToString();
        }

		/// <summary>
		/// GenerateImage sets the filename for each image using the abstract GenerateUrl method
		/// and generates the image data.
		/// </summary>
		/// <param name="images">The list of images on which to act.</param>
		protected virtual void GenerateImages(List<TiledImage> images)
		{
            // Generate image data
            // updating the rules is the responsibility of the caller
			int index = 0;
			foreach (TiledImage x in images)
			{
				x.SetFilename(GenerateUrl(index++, x));
				x.GenerateImage();
			}
		}
    }
}
